package com.excellent.search.service;

import com.excellent.item.pojo.*;
import com.excellent.search.client.BrandClient;
import com.excellent.search.client.CategoryClient;
import com.excellent.search.client.GoodsClient;
import com.excellent.search.client.SpecificationClient;
import com.excellent.search.pojo.Goods;
import com.excellent.search.pojo.SearchRequest;
import com.excellent.search.pojo.SearchResult;
import com.excellent.search.repository.GoodsRepository;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.NumberUtils;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.Operator;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.bucket.terms.LongTerms;
import org.elasticsearch.search.aggregations.bucket.terms.StringTerms;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage;
import org.springframework.data.elasticsearch.core.query.FetchSourceFilter;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.io.IOException;
import java.util.*;

/**
 * 【商城门户界面 - 搜索功能】
 * @author Xxacker
 */
@Slf4j
@Service
public class SearchService {

    @Autowired
    private BrandClient brandClient;

    @Autowired
    private CategoryClient categoryClient;  //获取三级分类名称，需要调用分类名称进行远程调用。

    @Autowired
    private GoodsClient goodsClient;

    @Autowired
    private SpecificationClient specificationClient;

    @Autowired
    private GoodsRepository goodsRepository;

    //将集合数据转化为 jackson 字符串的工具
    private static final ObjectMapper MAPPER = new ObjectMapper();

    /**
     * 将查询到的 Spu 转化为 Goods
     */
    public Goods buildGoods(Spu spu) throws IOException {
        // 创建goods对象
        Goods goods = new Goods();

        // 根据品牌 ID ，查询品牌
        Brand brand = this.brandClient.queryBrandById(spu.getBrandId());

        // 根据分类 ID ，查询分类名称；Arrays.asList("xxx", "yyy", "zzz")：用来获取cid组成的集合
        List<String> names = this.categoryClient.queryNamesByIds(Arrays.asList(spu.getCid1(), spu.getCid2(), spu.getCid3()));

        // 查询spu下的所有sku，获取启动的价格
        List<Sku> skus = this.goodsClient.querySkusBySpuId(spu.getId());
        List<Long> prices = new ArrayList<>();   //初始化一个价格，收集所有的sku的价格
        /*前端页面需要sku的Id，图片，价格，标题；封装方式：List<Map<String, Object>> */
        List<Map<String, Object>> skuMapList = new ArrayList<>();    //收集 sku 的必要字段信息
        // 遍历skus，获取价格集合
        skus.forEach(sku -> {
            prices.add(sku.getPrice());
            Map<String, Object> skuMap = new HashMap<>();
            skuMap.put("id", sku.getId());
            skuMap.put("title", sku.getTitle());
            skuMap.put("price", sku.getPrice());
            /* 获取 sku 中的图片，数据库的图片可能是多张，多张是以 "，" 进行分割，返回图片数组，
             * 获取第一张图片，如果没有图片的话，返回空字符串*/
            skuMap.put("image", StringUtils.isNotBlank(sku.getImages()) ? StringUtils.split(sku.getImages(), ",")[0] : "");
            skuMapList.add(skuMap);
        });

        // 查询出所有的搜索规格参数
        List<SpecParam> params = this.specificationClient.queryParams(null, spu.getCid3(), null, true);
        // 查询spuDetail。获取规格参数值
        SpuDetail spuDetail = this.goodsClient.querySpuDetailById(spu.getId());

        // 获取通用的规格参数(generic_spec)，需要进行反序列化，由于是复杂的集合对象
        Map<Long, Object> genericSpecMap = MAPPER.readValue(spuDetail.getGenericSpec(), new TypeReference<Map<Long, Object>>() { });
        // 获取特殊的规格参数，并对其进行反序列化
        Map<Long, List<Object>> specialSpecMap = MAPPER.readValue(spuDetail.getSpecialSpec(), new TypeReference<Map<Long, List<Object>>>() { });

        // 定义map接收：{规格参数名，规格参数值}
        Map<String, Object> specParamMap = new HashMap<>();
        params.forEach(param -> {
            // 判断是否通用规格参数
            if (param.getGeneric()) {
                // 获取通用规格参数值【待优化】
                String value = param.getId() == null ? "" : genericSpecMap.get(param.getId()).toString();
                // 判断是否是数值类型
                if (param.getNumeric()) {
                    // 如果是数值的话，判断该数值落在那个区间
                    value = chooseSegment(value, param);
                }
                // 把参数名和值放入结果集中
                specParamMap.put(param.getName(), value);
            } else {
                specParamMap.put(param.getName(), specialSpecMap.get(param.getId()));
            }
        });
        // 设置参数
        goods.setId(spu.getId());
        goods.setCid1(spu.getCid1());
        goods.setCid2(spu.getCid2());
        goods.setCid3(spu.getCid3());
        goods.setBrandId(spu.getBrandId());
        goods.setCreateTime(spu.getCreateTime());
        goods.setSubTitle(spu.getSubTitle());
        //拼接 All 字段，需要分类名称以及品牌名称；StringUtils.join(names, " ")：用来将 list 集合，转化为 json；中间添加空格，是防止两个品牌之间再形成一个新词
        goods.setAll(spu.getTitle() + brand.getName() + StringUtils.join(names, " "));
        //获取 spu 下所有 sku 的价格；因为将来任何一个价格满足条件，都应该被查询出来
        goods.setPrice(prices);
        //因为 Goods 实体类中的 skus 是字符串类型，所以获取 Spu 下所有 Sku ，需要转化为 json 字符串进行保存；也可以通过 MAPPER.readValue() 进行保存。
        goods.setSkus(MAPPER.writeValueAsString(skuMapList));
        //设置规格参数，获取所有查询的规格参数{name:value}，聚合处所有值，作为前台界面的可选项。
        goods.setSpecs(specParamMap);

        return goods;
    }

    /**
     * 判断数值应该落在哪个区间
     * */
    private String chooseSegment(String value, SpecParam p) {
        double val = NumberUtils.toDouble(value);
        String result = "其它";
        // 保存数值段
        for (String segment : p.getSegments().split(",")) {
            String[] segs = segment.split("-");
            // 获取数值范围
            double begin = NumberUtils.toDouble(segs[0]);
            double end = Double.MAX_VALUE;
            if(segs.length == 2){
                end = NumberUtils.toDouble(segs[1]);
            }
            // 判断是否在范围内
            if(val >= begin && val < end){
                if(segs.length == 1){
                    result = segs[0] + p.getUnit() + "以上";
                }else if(begin == 0){
                    result = segs[1] + p.getUnit() + "以下";
                }else{
                    result = segment + p.getUnit();
                }
                break;
            }
        }
        return result;
    }

    /**
     * 搜索的业务逻辑
     * @param request
     * @return
     */
    public SearchResult search(SearchRequest request) {
        String key = request.getKey();
        // 判断是否有搜索条件，如果没有，直接返回null。不允许搜索全部商品
        if (StringUtils.isBlank(key)) {
            return null;
        }

        // 自定义查询构建器，构建查询条件
        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
        // 添加查询条件，基本查询条件
        //QueryBuilder basicQuery = QueryBuilders.matchQuery("all", request.getKey()).operator(Operator.AND);
        // 添加过滤查询条件
        BoolQueryBuilder basicQuery = buildBooleanQueryBuilder(request);

        // 1、对key进行全文检索查询
        queryBuilder.withQuery(basicQuery);

        // 2、通过sourceFilter设置返回的结果字段,这里只需要id、skus、subTitle
        // 添加结果集过滤
        queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{"id","skus","subTitle"}, null));

        // 3、分页
        // 准备分页参数
        int page = request.getPage();
        int size = request.getSize();
        queryBuilder.withPageable(PageRequest.of(page - 1, size));    //由于分页页码是从0开始的，所以需要 -1

        // 4、添加分类和品牌的聚合 【以后需要根据聚合结果集进行解析】
        String categoryAggName = "categories";  //分类的聚合名称
        String brandAggName = "brands";         //品牌的聚合名称
        queryBuilder.addAggregation(AggregationBuilders.terms(categoryAggName).field("cid3"));  //cid3: 聚合的字段
        queryBuilder.addAggregation(AggregationBuilders.terms(brandAggName).field("brandId"));

        // 排序
        String sortBy = request.getSortBy();
        Boolean desc = request.getDescending() == null ? false : true;
        if (StringUtils.isNotBlank(sortBy)){
            // 如果不为空，则进行排序
            queryBuilder.withSort(SortBuilders.fieldSort(sortBy).order(desc ? SortOrder.DESC : SortOrder.ASC));
        }

        // 5、执行搜索，获取搜索的结果集
        //Page<Goods> goodsPage = this.goodsRepository.search(queryBuilder.build());    //普通的Page只能接收普通的结果集，无法获取聚合结果集
        //Page的子接口AggregatedPage，除了普通的普通的结果集，还扩展了聚合结果集
        AggregatedPage<Goods> goodsPage = (AggregatedPage<Goods>)this.goodsRepository.search(queryBuilder.build());

        // 6、解析聚合结果集
        List<Map<String, Object>> categories = getCategoryAggResult(goodsPage.getAggregation(categoryAggName));
        List<Brand> brands = getBrandAggResult(goodsPage.getAggregation(brandAggName));

        // 7、判断是否是一个分类，即判断分类聚合的结果集大小，只有一个分类时，才做规格参数的聚合
        List<Map<String, Object>> specs = null;
        if (categories.size() == 1) {
            // 根据规格参数聚合查询条件
            specs = getParamAggResult((Long)categories.get(0).get("id"), basicQuery);    //参数为：获取分类id，用户的查询条件
        }

        // 封装成需要的返回结果集
        return new SearchResult(goodsPage.getTotalElements(), goodsPage.getTotalPages(), goodsPage.getContent(), categories, brands, specs);
    }

    /**
     * 构建bool查询构建器
     * @param request
     * @return
     */
    private BoolQueryBuilder buildBooleanQueryBuilder(SearchRequest request) {
        //构建布尔查询构建器
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();

        // 添加基本查询条件
        boolQueryBuilder.must(QueryBuilders.matchQuery("all", request.getKey()).operator(Operator.AND));    //all为匹配的字段名

        // 添加过滤条件
        if (CollectionUtils.isEmpty(request.getFilter())){
            return boolQueryBuilder;
        }
        for (Map.Entry<String, Object> entry : request.getFilter().entrySet()) {
            String key = entry.getKey();
            // 如果过滤条件是“品牌”, 过滤的字段名：brandId
            if (StringUtils.equals("品牌", key)) {
                key = "brandId";
            } else if (StringUtils.equals("分类", key)) {
                // 如果是“分类”，过滤字段名：cid3
                key = "cid3";
            } else {
                // 如果是规格参数名，过滤字段名：specs.key.keyword
                key = "specs." + key + ".keyword";
            }
            //参数为过滤使用的字段名和字段值
            boolQueryBuilder.filter(QueryBuilders.termQuery(key, entry.getValue()));
        }

        return boolQueryBuilder;
    }

    /**
     * 聚合出规格参数过滤条件
     * @param id
     * @param basicQuery
     * @return
     */
    private List<Map<String,Object>> getParamAggResult(Long id, QueryBuilder basicQuery) {
        // 创建自定义查询构建器
        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
        // 基于基本的查询条件，聚合规格参数
        queryBuilder.withQuery(basicQuery);
        // 查询要聚合的规格参数
        List<SpecParam> params = this.specificationClient.queryParams(null, id, null, true);
        // 添加规格参数的聚合
        params.forEach(param -> {
            //规格参数时词条类型的聚合，param.getName()是聚合的名称；聚合字段是指定的 field() 中的参数【因为不需要进行分词，所以使用 ".keyword" 】
            queryBuilder.addAggregation(AggregationBuilders.terms(param.getName()).field("specs." + param.getName() + ".keyword"));
        });
        // 添加结果集过滤：只需要聚合结果集，不需要查询结果集
        queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{}, null));

        // 执行聚合查询,获取聚合结果集
        AggregatedPage<Goods> goodsPage = (AggregatedPage<Goods>)this.goodsRepository.search(queryBuilder.build());

        // 定义一个集合，收集聚合结果集
        List<Map<String, Object>> paramMapList = new ArrayList<>();
        // 解析聚合查询的结果集，【通过 asMap() 将其转化为一个 map 类型的结果集，key--聚合(规格参数名)、value--聚合对象】
        Map<String, Aggregation> aggregationMap = goodsPage.getAggregations() == null ? new HashMap<>() :goodsPage.getAggregations().asMap();
        //遍历map
        for (Map.Entry<String, Aggregation> entry : aggregationMap.entrySet()) {
            // 初始化 map {k:规格参数名 option：聚合的规格参数值}
            Map<String, Object> map = new HashMap<>();
            // 放入规格参数名
            map.put("k", entry.getKey());
            // 初始化一个 options 集合，收集桶中规格参数值key
            List<Object> options = new ArrayList<>();
            // 解析每个聚合
            StringTerms terms = (StringTerms)entry.getValue();
            // 遍历每个聚合中桶，把桶中key放入收集规格参数的集合中
            terms.getBuckets().forEach(bucket -> options.add(bucket.getKeyAsString()));
            map.put("options", options);
            paramMapList.add(map);
        }
        return paramMapList;
    }


    /**
     * 解析品牌聚合结果集
     * @param aggregation
     * @return
     */
    private List<Brand> getBrandAggResult(Aggregation aggregation) {
        // 处理聚合结果集
        LongTerms terms = (LongTerms)aggregation;
        // 获取所有的品牌id桶
        List<LongTerms.Bucket> buckets = terms.getBuckets();
        // 定义一个品牌集合，搜集所有的品牌对象
        List<Brand> brands = new ArrayList<>();
        // 解析所有的id桶，查询品牌
        buckets.forEach(bucket -> {
            Brand brand = this.brandClient.queryBrandById(bucket.getKeyAsNumber().longValue());
            brands.add(brand);
        });
        return brands;
    }

    /**
     * 解析分类的聚合结果集
     * @param aggregation
     * @return
     */
    private List<Map<String,Object>> getCategoryAggResult(Aggregation aggregation) {
        // 处理聚合结果集
        LongTerms terms = (LongTerms)aggregation;
        // 获取所有的分类id桶
        List<LongTerms.Bucket> buckets = terms.getBuckets();
        // 定义一个品牌集合，搜集所有的品牌对象
        List<Map<String, Object>> categories = new ArrayList<>();
        List<Long> cids = new ArrayList<>();
        // 解析所有的id桶，查询品牌
        buckets.forEach(bucket -> {
            cids.add(bucket.getKeyAsNumber().longValue());
        });
        List<String> names = this.categoryClient.queryNamesByIds(cids);
        for (int i = 0; i < cids.size(); i++) {
            Map<String, Object> map = new HashMap<>();
            map.put("id", cids.get(i));
            map.put("name", names.get(i));
            categories.add(map);
        }
        return categories;
    }

    /**
     * 创建索引
     * @param id
     * @throws IOException
     */
    public void createIndex(Long id) throws IOException {
        Spu spu = this.goodsClient.querySpuById(id);
        Goods goods = this.buildGoods(spu);      // 构建商品
        this.goodsRepository.save(goods);        // 保存数据到索引库
    }

    /**
     * 删除索引
     * @param id
     */
    public void deleteIndex(Long id) {
        this.goodsRepository.deleteById(id);
    }
}
